const fs = require('fs')
const path = require('path')

const { getConfig, getTestRoot } = require('./utils')
const Codecept = require('../codecept')
const container = require('../container')
const output = require('../output')
const actingHelpers = [...container.STANDARD_ACTING_HELPERS, 'REST']

/**
 * Prepare data and generate content of definitions file
 * @private
 *
 * @param {Object} params
 * @param {boolean} params.hasCustomStepsFile
 * @param {boolean} params.hasCustomHelper
 * @param {Map} params.supportObject
 * @param {Array<string>} params.helperNames
 * @param {Array<string>} params.importPaths
 * @param params.translations
 *
 * @returns {string}
 */
const getDefinitionsFileContent = ({ hasCustomHelper, hasCustomStepsFile, helperNames, supportObject, importPaths, translations }) => {
  const getHelperListFragment = ({ hasCustomHelper, hasCustomStepsFile }) => {
    if (hasCustomHelper && hasCustomStepsFile) {
      return `${['ReturnType<steps_file>', 'WithTranslation<Methods>'].join(', ')}`
    }

    if (hasCustomStepsFile) {
      return 'ReturnType<steps_file>'
    }

    return 'WithTranslation<Methods>'
  }

  const helpersListFragment = getHelperListFragment({
    hasCustomHelper,
    hasCustomStepsFile,
  })

  const importPathsFragment = importPaths.join('\n')
  const supportObjectsTypeFragment = convertMapToType(supportObject)
  const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : ''
  const translatedActionsFragment = JSON.stringify(translations.vocabulary.actions, null, 2)

  return generateDefinitionsContent({
    helpersListFragment,
    importPathsFragment,
    supportObjectsTypeFragment,
    methodsTypeFragment,
    translatedActionsFragment,
  })
}

/**
 * Generate content for definitions file from fragments
 * @private
 *
 * @param {Object} fragments - parts for template
 * @param {string} fragments.importPathsFragment
 * @param {string} fragments.supportObjectsTypeFragment
 * @param {string} fragments.methodsTypeFragment
 * @param {string} fragments.helpersListFragment
 * @param {string} fragments.translatedActionsFragment
 *
 * @returns {string}
 */
const generateDefinitionsContent = ({ importPathsFragment, supportObjectsTypeFragment, methodsTypeFragment, helpersListFragment, translatedActionsFragment }) => {
  return `/// <reference types='codeceptjs' />
${importPathsFragment}

declare namespace CodeceptJS {
  interface SupportObject ${supportObjectsTypeFragment}
  ${methodsTypeFragment}
  interface I extends ${helpersListFragment} {}
  namespace Translation {
    interface Actions ${translatedActionsFragment}
  }
}
`
}

/** @type {Array<string>} */
const helperNames = []
/** @type {Array<string>} */
const customHelpers = []

module.exports = function (genPath, options) {
  const configFile = options.config || genPath
  /** @type {string} */
  const testsPath = getTestRoot(configFile)
  const config = getConfig(configFile)
  if (!config) return

  /** @type {Object<string, string>} */
  const helperPaths = {}
  /** @type {Object<string, string>} */
  const supportPaths = {}
  /** @type {boolean} */
  let hasCustomStepsFile = false
  /** @type {boolean} */
  let hasCustomHelper = false

  /** @type {string} */
  const targetFolderPath = (options.output && getTestRoot(options.output)) || testsPath

  const codecept = new Codecept(config, {})
  codecept.init(testsPath)

  const helpers = container.helpers()
  const translations = container.translation()
  for (const name in helpers) {
    const require = codecept.config.helpers[name].require
    if (require) {
      helperPaths[name] = require
      helperNames.push(name)
    } else {
      const fullBasedPromised = codecept.config.fullPromiseBased
      helperNames.push(fullBasedPromised === true ? `${name}Ts` : name)
    }

    if (!actingHelpers.includes(name)) {
      customHelpers.push(name)
    }
  }

  let autoLogin
  if (config.plugins.autoLogin) {
    autoLogin = config.plugins.autoLogin.inject
  }

  const supportObject = new Map()
  supportObject.set('I', 'I')
  supportObject.set('current', 'any')

  if (translations.loaded) {
    supportObject.set(translations.I, translations.I)
  }

  if (autoLogin) {
    supportObject.set(autoLogin, 'any')
  }

  if (customHelpers.length > 0) {
    hasCustomHelper = true
  }

  for (const name in codecept.config.include) {
    const includePath = codecept.config.include[name]
    if (name === 'I' || name === translations.I) {
      hasCustomStepsFile = true
      supportPaths.steps_file = includePath
      continue
    }
    supportPaths[name] = includePath
    supportObject.set(name, name)
  }

  let definitionsFileContent = getDefinitionsFileContent({
    helperNames,
    supportObject,
    importPaths: getImportString(testsPath, targetFolderPath, supportPaths, helperPaths),
    translations,
    hasCustomStepsFile,
    hasCustomHelper,
  })

  // add aliases for translations
  if (translations.loaded) {
    const namespaceTranslationAliases = []
    namespaceTranslationAliases.push(`interface ${translations.vocabulary.I} extends WithTranslation<Methods> {}`)

    namespaceTranslationAliases.push('  namespace Translation {')
    definitionsFileContent = definitionsFileContent.replace('namespace Translation {', namespaceTranslationAliases.join('\n'))

    const translationAliases = []

    if (translations.vocabulary.contexts) {
      Object.keys(translations.vocabulary.contexts).forEach(k => {
        translationAliases.push(`declare const ${translations.vocabulary.contexts[k]}: typeof ${k};`)
      })
    }

    definitionsFileContent += `\n${translationAliases.join('\n')}`
  }

  if (options.dryRun) return

  fs.writeFileSync(path.join(targetFolderPath, 'steps.d.ts'), definitionsFileContent)
  output.print('TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs')
  output.print('Definitions were generated in steps.d.ts')
}

/**
 * Returns the relative path from the  to the targeted folder.
 * @param {string} originalPath
 * @param {string} targetFolderPath
 * @param {string} testsPath
 */
function getPath(originalPath, targetFolderPath, testsPath) {
  const parsedPath = path.parse(originalPath)

  // Remove typescript extension if exists.
  if (parsedPath.base.endsWith('.d.ts')) parsedPath.base = parsedPath.base.substring(0, parsedPath.base.length - 5)
  else if (parsedPath.ext === '.ts') parsedPath.base = parsedPath.name

  if (!parsedPath.dir.startsWith('.')) return path.posix.join(parsedPath.dir, parsedPath.base)
  const relativePath = path.posix.relative(
    targetFolderPath.split(path.sep).join(path.posix.sep),
    path.posix.join(testsPath.split(path.sep).join(path.posix.sep), parsedPath.dir.split(path.sep).join(path.posix.sep), parsedPath.base.split(path.sep).join(path.posix.sep)),
  )

  return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
}

/**
 *
 *
 * @param {string} testsPath
 * @param {string} targetFolderPath
 * @param {Object<string, string>} pathsToType
 * @param {Object<string, string>} pathsToValue
 *
 * @returns {Array<string>}
 */
function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue) {
  const importStrings = []

  for (const name in pathsToType) {
    const relativePath = getPath(pathsToType[name], targetFolderPath, testsPath)
    importStrings.push(`type ${name} = typeof import('${relativePath}');`)
  }

  for (const name in pathsToValue) {
    const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath)
    importStrings.push(`type ${name} = import('${relativePath}');`)
  }

  return importStrings
}

/**
 * @param {Map} map
 *
 * @returns {string}
 */
function convertMapToType(map) {
  return `{ ${Array.from(map)
    .map(([key, value]) => `${key}: ${value}`)
    .join(', ')} }`
}
